1use regex::Regex;
20use std::collections::HashMap;
21
22use std::sync::{Arc, RwLock, RwLockReadGuard};
23
24use super::error::Error as ErrorCode;
25use super::search::{CredentialSearch, CredentialSearchApi, CredentialSearchResult};
26
27lazy_static::lazy_static! {
28 static ref GLOBAL_CREDENTIAL_STORE: MockCredentialStore<MockData> = MockCredentialStore::default();
29}
30
31pub fn get_store() -> &'static MockCredentialStore<MockData> {
32 &GLOBAL_CREDENTIAL_STORE
33}
34
35pub trait CredentialFields {
36 fn service(&self) -> String;
37 fn target(&self) -> String;
38 fn user(&self) -> String;
39}
40
41pub trait CredentialStore<T> {
42 fn add(&self, credential: T);
43 fn get(&self) -> RwLockReadGuard<'_, Vec<Arc<T>>>;
44}
45
46#[derive(Debug)]
47pub struct MockCredentialStore<T: CredentialFields> {
48 inner: RwLock<Vec<Arc<T>>>,
49}
50
51impl<T: CredentialFields> Default for MockCredentialStore<T> {
52 fn default() -> MockCredentialStore<T> {
53 MockCredentialStore {
54 inner: RwLock::new(Vec::new()),
55 }
56 }
57}
58
59impl<T: CredentialFields> CredentialStore<T> for MockCredentialStore<T> {
60 fn add(&self, credential: T) {
61 let mut store = self
62 .inner
63 .write()
64 .expect("Rwlock poisoned in MockCredentialStore add method");
65 store.push(Arc::new(credential))
66 }
67 fn get(&self) -> RwLockReadGuard<'_, Vec<Arc<T>>> {
68 self.inner
69 .read()
70 .expect("Rwlock poisoned in MockCredentialStore get method")
71 }
72}
73
74#[derive(Debug)]
75pub struct MockData {
76 pub service: String,
77 pub target: String,
78 pub user: String,
79}
80
81impl CredentialFields for MockData {
82 fn service(&self) -> String {
83 self.service.clone()
84 }
85 fn target(&self) -> String {
86 self.target.clone()
87 }
88 fn user(&self) -> String {
89 self.user.clone()
90 }
91}
92
93pub struct MockCredentialSearch {}
94
95impl CredentialSearchApi for MockCredentialSearch {
96 fn by(&self, by: &str, query: &str) -> CredentialSearchResult {
97 let re = format!(r#"(?i){}"#, query);
98 let regex = match Regex::new(re.as_str()) {
99 Ok(regex) => regex,
100 Err(err) => return Err(ErrorCode::SearchError(format!("Regex Error, {}", err))),
101 };
102
103 match by.to_ascii_lowercase().as_str() {
104 "user" => search_by_user(regex),
105 "service" => search_by_service(regex),
106 "target" => search_by_target(regex),
107 _ => Err(ErrorCode::Unexpected("Mock by parameter".to_string())),
108 }
109 }
110}
111
112fn search_by_user(regex: Regex) -> CredentialSearchResult {
113 let store = get_store();
114 let data = match store.inner.write() {
115 Ok(data) => data,
116 Err(err) => {
117 return Err(ErrorCode::Unexpected(
118 format!("Poisoned MockCredentialStore in search by user: {}", err).to_string(),
119 ))
120 }
121 };
122 let mut count = 0;
123 let mut results = Vec::new();
124
125 let mut outer_map: HashMap<String, HashMap<String, String>> = HashMap::new();
126 let mut inner_map: HashMap<String, String> = HashMap::new();
127
128 for credential in data.iter() {
129 if regex.is_match(&credential.user()) {
130 results.push(credential);
131 }
132 }
133
134 for result in results {
135 count += 1;
136 inner_map.insert("User".to_string(), result.user.clone());
137 inner_map.insert("Service".to_string(), result.service.clone());
138 inner_map.insert("Target".to_string(), result.target.clone());
139 outer_map.insert(count.to_string(), inner_map.clone());
140 }
141
142 if count == 0 {
143 return Err(ErrorCode::NoResults);
144 }
145
146 Ok(outer_map)
147}
148fn search_by_service(regex: Regex) -> CredentialSearchResult {
149 let store = get_store();
150 let data = match store.inner.write() {
151 Ok(data) => data,
152 Err(err) => {
153 return Err(ErrorCode::Unexpected(
154 format!("Poisoned MockCredentialStore in search by user: {}", err).to_string(),
155 ))
156 }
157 };
158 let mut count = 0;
159 let mut results = Vec::new();
160
161 let mut outer_map: HashMap<String, HashMap<String, String>> = HashMap::new();
162 let mut inner_map: HashMap<String, String> = HashMap::new();
163
164 for credential in data.iter() {
165 if regex.is_match(&credential.service()) {
166 results.push(credential);
167 }
168 }
169
170 for result in results {
171 count += 1;
172 inner_map.insert("User".to_string(), result.user.clone());
173 inner_map.insert("Service".to_string(), result.service.clone());
174 inner_map.insert("Target".to_string(), result.target.clone());
175 outer_map.insert(count.to_string(), inner_map.clone());
176 }
177
178 if count == 0 {
179 return Err(ErrorCode::NoResults);
180 }
181
182 Ok(outer_map)
183}
184fn search_by_target(regex: Regex) -> CredentialSearchResult {
185 let store = get_store();
186 let data = match store.inner.write() {
187 Ok(data) => data,
188 Err(err) => {
189 return Err(ErrorCode::Unexpected(
190 format!("Poisoned MockCredentialStore in search by user: {}", err).to_string(),
191 ))
192 }
193 };
194 let mut count = 0;
195 let mut results = Vec::new();
196
197 let mut outer_map: HashMap<String, HashMap<String, String>> = HashMap::new();
198 let mut inner_map: HashMap<String, String> = HashMap::new();
199
200 for credential in data.iter() {
201 if regex.is_match(&credential.target()) {
202 results.push(credential);
203 }
204 }
205
206 for result in results {
207 count += 1;
208 inner_map.insert("User".to_string(), result.user.clone());
209 inner_map.insert("Service".to_string(), result.service.clone());
210 inner_map.insert("Target".to_string(), result.target.clone());
211 outer_map.insert(count.to_string(), inner_map.clone());
212 }
213
214 if count == 0 {
215 return Err(ErrorCode::NoResults);
216 }
217
218 Ok(outer_map)
219}
220
221pub fn default_credential_search() -> Box<CredentialSearch> {
222 Box::new(MockCredentialSearch {})
223}
224
225#[cfg(test)]
226mod tests {
227 use super::{get_store, MockData};
228 use crate::mock::CredentialStore;
229 use crate::tests::generate_random_string;
230 use crate::{mock, set_default_credential_search, Error, Limit, List};
231 use std::collections::HashSet;
232
233 fn searchable_entries() -> [String; 4] {
234 let store = get_store();
235
236 let name1 = generate_random_string();
237 let name2 = generate_random_string();
238 let name3 = generate_random_string();
239 let name4 = generate_random_string();
240
241 let data1 = MockData {
242 service: name1.clone(),
243 target: name1.clone(),
244 user: name1.clone(),
245 };
246
247 let data2 = MockData {
248 service: name2.clone(),
249 target: name2.clone(),
250 user: name2.clone(),
251 };
252
253 let data3 = MockData {
254 service: name3.clone(),
255 target: name3.clone(),
256 user: name3.clone(),
257 };
258
259 let data4 = MockData {
260 service: name4.clone(),
261 target: name4.clone(),
262 user: name4.clone(),
263 };
264
265 store.add(data1);
266 store.add(data2);
267 store.add(data3);
268 store.add(data4);
269
270 [name1, name2, name3, name4]
271 }
272
273 #[test]
274 fn test_mock_search_by_user() {
275 let names = searchable_entries();
276 let result = set_default_credential_search(mock::default_credential_search())
277 .expect("Failed to create mock search")
278 .by_user(&names[1]);
279
280 let list = List::list_credentials(&result, Limit::All);
281
282 let expected_str = format!(
283 "1\nTarget: {}\nService: {}\nUser: {}\n",
284 &names[1], &names[1], &names[1]
285 );
286
287 let expected_set: HashSet<&str> = expected_str.lines().collect();
288 let result_set: HashSet<&str> = list.lines().collect();
289
290 assert_eq!(
291 expected_set, result_set,
292 "Search result and expected result do not match"
293 );
294 }
295
296 #[test]
297 fn test_mock_search_by_target() {
298 let names = searchable_entries();
299 let result = set_default_credential_search(mock::default_credential_search())
300 .expect("Failed to create mock search")
301 .by_target(&names[1]);
302
303 let list = List::list_credentials(&result, Limit::All);
304
305 let expected_str = format!(
306 "1\nTarget: {}\nService: {}\nUser: {}\n",
307 &names[1], &names[1], &names[1]
308 );
309
310 let expected_set: HashSet<&str> = expected_str.lines().collect();
311 let result_set: HashSet<&str> = list.lines().collect();
312
313 assert_eq!(
314 expected_set, result_set,
315 "Search result and expected result do not match"
316 );
317 }
318
319 #[test]
320 fn test_mock_search_by_service() {
321 let names = searchable_entries();
322 let result = set_default_credential_search(mock::default_credential_search())
323 .expect("Failed to create mock search")
324 .by_service(&names[1]);
325
326 let list = List::list_credentials(&result, Limit::All);
327
328 let expected_str = format!(
329 "1\nTarget: {}\nService: {}\nUser: {}\n",
330 &names[1], &names[1], &names[1]
331 );
332
333 let expected_set: HashSet<&str> = expected_str.lines().collect();
334 let result_set: HashSet<&str> = list.lines().collect();
335
336 assert_eq!(
337 expected_set, result_set,
338 "Search result and expected result do not match"
339 );
340 }
341
342 #[test]
343 fn no_results() {
344 let name = generate_random_string();
345 let result = set_default_credential_search(mock::default_credential_search())
346 .expect("Failed to create mock search")
347 .by_service(&name)
348 .unwrap_err();
349
350 assert!(matches!(result, Error::NoResults));
351 }
352
353 #[test]
354 fn test_max_result() {
355 let name = generate_random_string();
356 let store = get_store();
357 let credential1 = MockData {
358 service: "test-service1".to_string(),
359 target: "test-target1".to_string(),
360 user: name.clone(),
361 };
362
363 let credential2 = MockData {
364 service: "test-service2".to_string(),
365 target: "test-target2".to_string(),
366 user: name.clone(),
367 };
368
369 let credential3 = MockData {
370 service: "test-service3".to_string(),
371 target: "test-target3".to_string(),
372 user: name.clone(),
373 };
374 store.add(credential1);
375 store.add(credential2);
376 store.add(credential3);
377 let result = set_default_credential_search(mock::default_credential_search())
378 .expect("Failed to create mock search")
379 .by_user(&name);
380
381 let list = List::list_credentials(&result, Limit::Max(2));
382
383 let result_set = list.lines().count();
384
385 assert_eq!(8, result_set);
386 }
387}