keyring_core/
mock.rs

1/*!
2
3# Mock credential store
4
5To facilitate testing of clients, this crate provides a Mock credential store
6that is platform-independent, provides no persistence, and allows the client
7to specify the return values (including errors) for each call. The credentials
8in this store have no attributes at all.
9
10To use this credential store instead of the default, make this call during
11application startup _before_ creating any entries:
12```rust
13keyring_core::set_default_store(keyring_core::mock::Store::new().unwrap());
14```
15
16You can then create entries as usual and call their usual methods
17to set, get, and delete passwords. There is no persistence except in-memory
18so, once you drop the store, all the credentials will be gone.
19
20If you want a method call on an entry to fail in a specific way, you can
21downcast the entry to a [Cred] and then call [set_error](Cred::set_error)
22with the appropriate error.  The next entry method called on the credential
23will fail with the error you set.  The error will then be cleared, so the next
24call on the mock will operate as usual.  Setting an error will not affect
25the value of the credential (if any). Here's a complete example:
26
27```rust
28# use keyring_core::{Entry, Error, mock};
29keyring_core::set_default_store(mock::Store::new().unwrap());
30let entry = Entry::new("service", "user").unwrap();
31entry.set_password("test").expect("the entry's password is now test");
32let mock: &mock::Cred = entry.as_any().downcast_ref().unwrap();
33mock.set_error(Error::Invalid("mock error".to_string(), "takes precedence".to_string()));
34_ = entry.get_password().expect_err("the error will be returned");
35let val = entry.get_password().expect("the error has been cleared");
36assert_eq!(val, "test", "the error did not affect the password");
37```
38
39 */
40use std::cell::RefCell;
41use std::collections::HashMap;
42use std::sync::{Arc, Mutex};
43use std::time::{Duration, SystemTime, UNIX_EPOCH};
44
45use crate::api::{CredentialApi, CredentialStoreApi};
46use crate::{Credential, CredentialPersistence, Entry, Error, Result};
47
48/// The concrete mock credential
49///
50/// Mocks use an internal mutability pattern since entries are read-only.
51/// The mutex is used to make sure these are Sync.
52#[derive(Debug)]
53pub struct Cred {
54    pub specifiers: (String, String),
55    pub inner: Mutex<RefCell<CredData>>,
56}
57
58/// The (in-memory) persisted data for a mock credential.
59///
60/// We keep a password but, unlike most credentials stores,
61/// we also keep an intended error to return on the next call.
62///
63/// (Everything about this structure is public for transparency.
64/// Most credential store implementations hide their internals.)
65#[derive(Debug, Default)]
66pub struct CredData {
67    pub secret: Option<Vec<u8>>,
68    pub error: Option<Error>,
69}
70
71impl CredentialApi for Cred {
72    /// See the API docs.
73    ///
74    /// If there is an error in the mock, it will be returned
75    /// and the secret will _not_ be set.  The error will
76    /// be cleared, so calling again will set the secret.
77    fn set_secret(&self, secret: &[u8]) -> Result<()> {
78        let mut inner = self
79            .inner
80            .lock()
81            .expect("Can't access mock data for set_secret: please report a bug!");
82        let data = inner.get_mut();
83        let err = data.error.take();
84        match err {
85            None => {
86                data.secret = Some(secret.to_vec());
87                Ok(())
88            }
89            Some(err) => Err(err),
90        }
91    }
92
93    /// See the API docs.
94    ///
95    /// If there is an error set in the mock, it will
96    /// be returned instead of a secret. The existing
97    /// secret will not change.
98    fn get_secret(&self) -> Result<Vec<u8>> {
99        let mut inner = self
100            .inner
101            .lock()
102            .expect("Can't access mock data for get: please report a bug!");
103        let data = inner.get_mut();
104        let err = data.error.take();
105        match err {
106            None => match &data.secret {
107                None => Err(Error::NoEntry),
108                Some(val) => Ok(val.clone()),
109            },
110            Some(err) => Err(err),
111        }
112    }
113
114    /// See the API docs.
115    ///
116    /// If there is an error, it will be returned and
117    /// cleared. Calling again will delete the cred.
118    fn delete_credential(&self) -> Result<()> {
119        let mut inner = self
120            .inner
121            .lock()
122            .expect("Can't access mock data for delete: please report a bug!");
123        let data = inner.get_mut();
124        let err = data.error.take();
125        match err {
126            None => match data.secret {
127                Some(_) => {
128                    data.secret = None;
129                    Ok(())
130                }
131                None => Err(Error::NoEntry),
132            },
133            Some(err) => Err(err),
134        }
135    }
136
137    /// See the API docs.
138    ///
139    /// If there is an error in the mock, it's returned instead and cleared.
140    /// Calling again will retry the operation.
141    fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
142        let mut inner = self
143            .inner
144            .lock()
145            .expect("Can't access mock data for get_credential: please report a bug!");
146        let data = inner.get_mut();
147        let err = data.error.take();
148        match err {
149            None => match data.secret {
150                Some(_) => Ok(None),
151                None => Err(Error::NoEntry),
152            },
153            Some(err) => Err(err),
154        }
155    }
156
157    /// See the API docs.
158    fn get_specifiers(&self) -> Option<(String, String)> {
159        Some(self.specifiers.clone())
160    }
161
162    /// Return this mock credential concrete object
163    /// wrapped in the [Any](std::any::Any) trait,
164    /// so it can be downcast.
165    fn as_any(&self) -> &dyn std::any::Any {
166        self
167    }
168
169    /// Expose the concrete debug formatter for use via the [Credential] trait
170    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        std::fmt::Debug::fmt(self, f)
172    }
173}
174
175impl Cred {
176    /// Set an error to be returned from this mock credential.
177    ///
178    /// Error returns always take precedence over the normal
179    /// behavior of the mock.  But once an error has been
180    /// returned, it is removed, so the mock works thereafter.
181    pub fn set_error(&self, err: Error) {
182        let mut inner = self
183            .inner
184            .lock()
185            .expect("Can't access mock data for set_error: please report a bug!");
186        let data = inner.get_mut();
187        data.error = Some(err);
188    }
189}
190
191/// The builder for mock credentials.
192///
193/// We keep them in a vector so we can reuse them
194/// for entries with the same service and user.
195/// Yes, a hashmap might be faster, but this is
196/// way simpler.
197#[derive(Debug)]
198pub struct Store {
199    pub id: String,
200    pub inner: Mutex<RefCell<Vec<Arc<Cred>>>>,
201}
202
203impl Store {
204    pub fn new() -> Result<Arc<Self>> {
205        Ok(Arc::new(Store {
206            id: format!(
207                "Crate version {}, Instantiated at {}",
208                env!("CARGO_PKG_VERSION"),
209                SystemTime::now()
210                    .duration_since(UNIX_EPOCH)
211                    .unwrap_or_else(|_| Duration::new(0, 0))
212                    .as_secs_f64()
213            ),
214            inner: Mutex::new(RefCell::new(Vec::new())),
215        }))
216    }
217}
218
219impl CredentialStoreApi for Store {
220    fn vendor(&self) -> String {
221        String::from("Mock in-memory store, https://crates.io/crates/keyring-core")
222    }
223
224    fn id(&self) -> String {
225        self.id.clone()
226    }
227
228    /// Build a mock credential for the service and user. No modifiers are allowed.
229    ///
230    /// Since mocks don't persist beyond the life of their entry, all mocks
231    /// start off without passwords.
232    fn build(
233        &self,
234        service: &str,
235        user: &str,
236        mods: Option<&HashMap<&str, &str>>,
237    ) -> Result<Entry> {
238        if mods.is_some_and(|m| !m.is_empty()) {
239            return Err(Error::NotSupportedByStore(
240                "The mock store doesn't allow modifiers".to_string(),
241            ));
242        }
243        let mut inner = self
244            .inner
245            .lock()
246            .expect("Can't access mock store data: please report a bug!");
247        let creds = inner.get_mut();
248        for cred in creds.iter() {
249            if service == cred.specifiers.0 && user == cred.specifiers.1 {
250                return Ok(Entry {
251                    inner: cred.clone(),
252                });
253            }
254        }
255        let cred = Arc::new(Cred {
256            specifiers: (service.to_string(), user.to_string()),
257            inner: Mutex::new(RefCell::new(Default::default())),
258        });
259        creds.push(cred.clone());
260        Ok(Entry { inner: cred })
261    }
262
263    /// Search for mock credentials matching the spec.
264    ///
265    /// Attributes other than `service` and `user` are ignored.
266    /// Their values are used in unanchored substring searches against the specifier.
267    fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
268        let mut result: Vec<Entry> = Vec::new();
269        let svc = spec.get("service").unwrap_or(&"");
270        let usr = spec.get("user").unwrap_or(&"");
271        let mut inner = self
272            .inner
273            .lock()
274            .expect("Can't access mock store data: please report a bug!");
275        let creds = inner.get_mut();
276        for cred in creds.iter() {
277            if !cred.specifiers.0.as_str().contains(svc) {
278                continue;
279            }
280            if !cred.specifiers.1.as_str().contains(usr) {
281                continue;
282            }
283            result.push(Entry {
284                inner: cred.clone(),
285            });
286        }
287        Ok(result)
288    }
289
290    /// Get an [Any][std::any::Any] reference to the mock credential builder.
291    fn as_any(&self) -> &dyn std::any::Any {
292        self
293    }
294
295    /// This keystore keeps the password in the entry!
296    fn persistence(&self) -> CredentialPersistence {
297        CredentialPersistence::ProcessOnly
298    }
299
300    /// Expose the concrete debug formatter
301    /// for use via the [CredentialStore](crate::CredentialStore) trait
302    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        std::fmt::Debug::fmt(self, f)
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use std::sync::{Arc, Once};
310
311    use super::{Cred, HashMap, Store};
312    use crate::{CredentialPersistence, CredentialStore, Entry, Error, get_default_store};
313
314    static SET_STORE: Once = Once::new();
315
316    fn usually_goes_in_main() {
317        let _ = env_logger::builder().is_test(true).try_init();
318        crate::set_default_store(Store::new().unwrap());
319    }
320
321    #[test]
322    fn test_store_methods() {
323        SET_STORE.call_once(usually_goes_in_main);
324        let store = get_default_store().unwrap();
325        let vendor1 = store.vendor();
326        let id1 = store.id();
327        let vendor2 = store.vendor();
328        let id2 = store.id();
329        assert_eq!(vendor1, vendor2);
330        assert_eq!(id1, id2);
331        let store2: Arc<CredentialStore> = Store::new().unwrap();
332        let vendor3 = store2.vendor();
333        let id3 = store2.id();
334        assert_eq!(vendor1, vendor3);
335        assert_ne!(id1, id3);
336    }
337
338    fn entry_new(service: &str, user: &str) -> Entry {
339        SET_STORE.call_once(usually_goes_in_main);
340        Entry::new(service, user).unwrap_or_else(|err| {
341            panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
342        })
343    }
344
345    fn generate_random_string() -> String {
346        use fastrand;
347        use std::iter::repeat_with;
348        repeat_with(fastrand::alphanumeric).take(30).collect()
349    }
350
351    fn generate_random_bytes() -> Vec<u8> {
352        use fastrand;
353        use std::iter::repeat_with;
354        repeat_with(|| fastrand::u8(..)).take(24).collect()
355    }
356
357    // A round-trip password test that doesn't delete the credential afterward
358    fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
359        entry
360            .set_password(in_pass)
361            .unwrap_or_else(|err| panic!("Can't set password: {case}: {err:?}"));
362        let out_pass = entry
363            .get_password()
364            .unwrap_or_else(|err| panic!("Can't get password: {case}: {err:?}"));
365        assert_eq!(
366            in_pass, out_pass,
367            "Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
368        )
369    }
370
371    // A round-trip password test that does delete the credential afterward
372    fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
373        test_round_trip_no_delete(case, entry, in_pass);
374        entry
375            .delete_credential()
376            .unwrap_or_else(|err| panic!("Can't delete password: {case}: {err:?}"));
377        let password = entry.get_password();
378        assert!(matches!(password, Err(Error::NoEntry)));
379    }
380
381    // A round-trip secret test that does delete the credential afterward
382    pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
383        entry
384            .set_secret(in_secret)
385            .unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
386        let out_secret = entry
387            .get_secret()
388            .unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
389        assert_eq!(
390            in_secret, &out_secret,
391            "Secrets don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
392        );
393        entry
394            .delete_credential()
395            .unwrap_or_else(|err| panic!("Can't delete credential for {case}: {err:?}"));
396        let secret = entry.get_secret();
397        assert!(matches!(secret, Err(Error::NoEntry)));
398    }
399
400    #[test]
401    fn test_empty_service_and_user() {
402        let name = generate_random_string();
403        let in_pass = "value doesn't matter";
404        test_round_trip("empty user", &entry_new(&name, ""), in_pass);
405        test_round_trip("empty service", &entry_new("", &name), in_pass);
406        test_round_trip("empty service and user", &entry_new("", ""), in_pass);
407    }
408
409    #[test]
410    fn test_empty_password() {
411        let name = generate_random_string();
412        let in_pass = "";
413        test_round_trip("empty password", &entry_new(&name, &name), in_pass);
414    }
415
416    #[test]
417    fn test_missing_entry() {
418        let name = generate_random_string();
419        let entry = entry_new(&name, &name);
420        assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
421    }
422
423    #[test]
424    fn test_round_trip_ascii_password() {
425        let name = generate_random_string();
426        let entry = entry_new(&name, &name);
427        test_round_trip("ascii password", &entry, "test ascii password");
428    }
429
430    #[test]
431    fn test_round_trip_non_ascii_password() {
432        let name = generate_random_string();
433        let entry = entry_new(&name, &name);
434        test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
435    }
436
437    #[test]
438    fn test_entries_with_same_and_different_specifiers() {
439        let name1 = generate_random_string();
440        let name2 = generate_random_string();
441        let entry1 = entry_new(&name1, &name2);
442        let entry2 = entry_new(&name1, &name2);
443        let entry3 = entry_new(&name2, &name1);
444        entry1.set_password("test password").unwrap();
445        let pw2 = entry2.get_password().unwrap();
446        assert_eq!(pw2, "test password");
447        _ = entry3.get_password().unwrap_err();
448        entry1.delete_credential().unwrap();
449        _ = entry2.get_password().unwrap_err();
450        entry3.delete_credential().unwrap_err();
451    }
452
453    #[test]
454    fn test_get_credential_and_specifiers() {
455        let name = generate_random_string();
456        let entry1 = entry_new(&name, &name);
457        assert!(matches!(entry1.get_credential(), Err(Error::NoEntry)));
458        entry1.set_password("password for entry1").unwrap();
459        let wrapper = entry1.get_credential().unwrap();
460        let (service, user) = wrapper.get_specifiers().unwrap();
461        assert_eq!(service, name);
462        assert_eq!(user, name);
463        wrapper.delete_credential().unwrap();
464        entry1.delete_credential().unwrap_err();
465        wrapper.delete_credential().unwrap_err();
466    }
467
468    #[test]
469    fn test_round_trip_random_secret() {
470        let name = generate_random_string();
471        let entry = entry_new(&name, &name);
472        let secret = generate_random_bytes();
473        test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
474    }
475
476    #[test]
477    fn test_update() {
478        let name = generate_random_string();
479        let entry = entry_new(&name, &name);
480        test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
481        test_round_trip(
482            "updated non-ascii password",
483            &entry,
484            "このきれいな花は桜です",
485        );
486    }
487
488    #[test]
489    fn test_set_error() {
490        let name = generate_random_string();
491        let entry = entry_new(&name, &name);
492        let password = "test ascii password";
493        let mock: &Cred = entry.inner.as_any().downcast_ref().unwrap();
494        mock.set_error(Error::Invalid(
495            "mock error".to_string(),
496            "is an error".to_string(),
497        ));
498        assert!(matches!(
499            entry.set_password(password),
500            Err(Error::Invalid(_, _))
501        ));
502        entry.set_password(password).unwrap();
503        mock.set_error(Error::NoEntry);
504        assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
505        let stored_password = entry.get_password().unwrap();
506        assert_eq!(stored_password, password);
507        mock.set_error(Error::TooLong("mock".to_string(), 3));
508        assert!(matches!(
509            entry.delete_credential(),
510            Err(Error::TooLong(_, 3))
511        ));
512        entry.delete_credential().unwrap();
513        assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
514    }
515
516    #[test]
517    fn test_search() {
518        let store: Arc<CredentialStore> = Store::new().unwrap();
519        let all = store.search(&HashMap::from([])).unwrap();
520        assert!(all.is_empty());
521        let all = store
522            .search(&HashMap::from([("service", ""), ("user", "")]))
523            .unwrap();
524        assert!(all.is_empty());
525        let e1 = store.build("foo", "bar", None).unwrap();
526        e1.set_password("e1").unwrap();
527        let all = store.search(&HashMap::from([])).unwrap();
528        assert_eq!(all.len(), 1);
529        let all = store
530            .search(&HashMap::from([("service", ""), ("user", "")]))
531            .unwrap();
532        assert_eq!(all.len(), 1);
533        let e2 = store.build("foo", "bam", None).unwrap();
534        e2.set_password("e2").unwrap();
535        let one = store.search(&HashMap::from([("user", "m")])).unwrap();
536        assert_eq!(one.len(), 1);
537        let one = store
538            .search(&HashMap::from([("service", "foo"), ("user", "bar")]))
539            .unwrap();
540        assert_eq!(one.len(), 1);
541        let two = store.search(&HashMap::from([("service", "foo")])).unwrap();
542        assert_eq!(two.len(), 2);
543        let all = store.search(&HashMap::from([("foo", "bar")])).unwrap();
544        assert_eq!(all.len(), 2);
545    }
546
547    #[test]
548    fn test_persistence() {
549        let store: Arc<CredentialStore> = Store::new().unwrap();
550        assert!(matches!(
551            store.persistence(),
552            CredentialPersistence::ProcessOnly
553        ))
554    }
555}