Skip to main content

keyring_search/
mock.rs

1/*!
2# Mock Credential searching mechanism
3
4This module creates a mock credential store to for testing a credential
5search. The other modules, aside from Windows, have built in search APIs that
6keyring-search utilizes. The mock credential search mimics the custom regex
7that the Windows module uses to search through all credentials.
8
9To use this search instead of default, make this call during application startup:
10```rust
11use keyring_search::{set_default_credential_search, mock};
12set_default_credential_search(mock::default_credential_search());
13```
14
15The mock module creates a default credential store that can be searched with the
16keyring-search API.
17 */
18
19use 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}