Skip to main content

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.
197pub struct Store {
198    pub id: String,
199    pub inner: Mutex<RefCell<Vec<Arc<Cred>>>>,
200}
201
202impl std::fmt::Debug for Store {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        f.debug_struct("Store")
205            .field("vendor", &self.vendor())
206            .field("id", &self.id)
207            .finish()
208    }
209}
210
211impl Store {
212    pub fn new() -> Result<Arc<Self>> {
213        Ok(Arc::new(Store {
214            id: format!(
215                "Crate version {}, Instantiated at {}",
216                env!("CARGO_PKG_VERSION"),
217                SystemTime::now()
218                    .duration_since(UNIX_EPOCH)
219                    .unwrap_or_else(|_| Duration::new(0, 0))
220                    .as_secs_f64()
221            ),
222            inner: Mutex::new(RefCell::new(Vec::new())),
223        }))
224    }
225}
226
227impl CredentialStoreApi for Store {
228    fn vendor(&self) -> String {
229        String::from("Mock store, https://crates.io/crates/keyring-core")
230    }
231
232    fn id(&self) -> String {
233        self.id.clone()
234    }
235
236    /// Build a mock credential for the service and user. No modifiers are allowed.
237    ///
238    /// Since mocks don't persist beyond the life of their entry, all mocks
239    /// start off without passwords.
240    fn build(
241        &self,
242        service: &str,
243        user: &str,
244        mods: Option<&HashMap<&str, &str>>,
245    ) -> Result<Entry> {
246        if mods.is_some_and(|m| !m.is_empty()) {
247            let msg = "The mock store doesn't allow entry modifiers";
248            return Err(Error::NotSupportedByStore(msg.to_string()));
249        }
250        let mut inner = self
251            .inner
252            .lock()
253            .expect("Can't access mock store data: please report a bug!");
254        let creds = inner.get_mut();
255        for cred in creds.iter() {
256            if service == cred.specifiers.0 && user == cred.specifiers.1 {
257                return Ok(Entry {
258                    inner: cred.clone(),
259                });
260            }
261        }
262        let cred = Arc::new(Cred {
263            specifiers: (service.to_string(), user.to_string()),
264            inner: Mutex::new(RefCell::new(Default::default())),
265        });
266        creds.push(cred.clone());
267        Ok(Entry { inner: cred })
268    }
269
270    /// Search for mock credentials matching the spec.
271    ///
272    /// Attributes other than `service` and `user` are ignored.
273    /// Their values are used in unanchored substring searches against the specifier.
274    fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
275        let mut result: Vec<Entry> = Vec::new();
276        let svc = spec.get("service").unwrap_or(&"");
277        let usr = spec.get("user").unwrap_or(&"");
278        let mut inner = self
279            .inner
280            .lock()
281            .expect("Can't access mock store data: please report a bug!");
282        let creds = inner.get_mut();
283        for cred in creds.iter() {
284            if !cred.specifiers.0.as_str().contains(svc) {
285                continue;
286            }
287            if !cred.specifiers.1.as_str().contains(usr) {
288                continue;
289            }
290            result.push(Entry {
291                inner: cred.clone(),
292            });
293        }
294        Ok(result)
295    }
296
297    /// Get an [Any][std::any::Any] reference to the mock credential builder.
298    fn as_any(&self) -> &dyn std::any::Any {
299        self
300    }
301
302    /// This keystore keeps the password in the entry!
303    fn persistence(&self) -> CredentialPersistence {
304        CredentialPersistence::ProcessOnly
305    }
306
307    /// Expose the concrete debug formatter
308    /// for use via the [CredentialStore](crate::CredentialStore) trait
309    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        std::fmt::Debug::fmt(self, f)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use std::sync::{Arc, Once};
317
318    use super::{Cred, HashMap, Store};
319    use crate::{CredentialPersistence, CredentialStore, Entry, Error, get_default_store};
320
321    static SET_STORE: Once = Once::new();
322
323    fn usually_goes_in_main() {
324        let _ = env_logger::builder().is_test(true).try_init();
325        crate::set_default_store(Store::new().unwrap());
326    }
327
328    #[test]
329    fn test_store_methods() {
330        SET_STORE.call_once(usually_goes_in_main);
331        let store = get_default_store().unwrap();
332        let vendor1 = store.vendor();
333        let id1 = store.id();
334        let vendor2 = store.vendor();
335        let id2 = store.id();
336        assert_eq!(vendor1, vendor2);
337        assert_eq!(id1, id2);
338        let store2: Arc<CredentialStore> = Store::new().unwrap();
339        let vendor3 = store2.vendor();
340        let id3 = store2.id();
341        assert_eq!(vendor1, vendor3);
342        assert_ne!(id1, id3);
343    }
344
345    fn entry_new(service: &str, user: &str) -> Entry {
346        SET_STORE.call_once(usually_goes_in_main);
347        Entry::new(service, user).unwrap_or_else(|err| {
348            panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
349        })
350    }
351
352    fn generate_random_string() -> String {
353        use fastrand;
354        use std::iter::repeat_with;
355        repeat_with(fastrand::alphanumeric).take(30).collect()
356    }
357
358    fn generate_random_bytes() -> Vec<u8> {
359        use fastrand;
360        use std::iter::repeat_with;
361        repeat_with(|| fastrand::u8(..)).take(24).collect()
362    }
363
364    // A round-trip password test that doesn't delete the credential afterward
365    fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
366        entry
367            .set_password(in_pass)
368            .unwrap_or_else(|err| panic!("Can't set password: {case}: {err:?}"));
369        let out_pass = entry
370            .get_password()
371            .unwrap_or_else(|err| panic!("Can't get password: {case}: {err:?}"));
372        assert_eq!(
373            in_pass, out_pass,
374            "Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
375        )
376    }
377
378    // A round-trip password test that does delete the credential afterward
379    fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
380        test_round_trip_no_delete(case, entry, in_pass);
381        entry
382            .delete_credential()
383            .unwrap_or_else(|err| panic!("Can't delete password: {case}: {err:?}"));
384        let password = entry.get_password();
385        assert!(matches!(password, Err(Error::NoEntry)));
386    }
387
388    // A round-trip secret test that does delete the credential afterward
389    pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
390        entry
391            .set_secret(in_secret)
392            .unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
393        let out_secret = entry
394            .get_secret()
395            .unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
396        assert_eq!(
397            in_secret, &out_secret,
398            "Secrets don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
399        );
400        entry
401            .delete_credential()
402            .unwrap_or_else(|err| panic!("Can't delete credential for {case}: {err:?}"));
403        let secret = entry.get_secret();
404        assert!(matches!(secret, Err(Error::NoEntry)));
405    }
406
407    #[test]
408    fn test_empty_service_and_user() {
409        let name = generate_random_string();
410        let in_pass = "value doesn't matter";
411        test_round_trip("empty user", &entry_new(&name, ""), in_pass);
412        test_round_trip("empty service", &entry_new("", &name), in_pass);
413        test_round_trip("empty service and user", &entry_new("", ""), in_pass);
414    }
415
416    #[test]
417    fn test_empty_password() {
418        let name = generate_random_string();
419        let in_pass = "";
420        test_round_trip("empty password", &entry_new(&name, &name), in_pass);
421    }
422
423    #[test]
424    fn test_missing_entry() {
425        let name = generate_random_string();
426        let entry = entry_new(&name, &name);
427        assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
428    }
429
430    #[test]
431    fn test_round_trip_ascii_password() {
432        let name = generate_random_string();
433        let entry = entry_new(&name, &name);
434        test_round_trip("ascii password", &entry, "test ascii password");
435    }
436
437    #[test]
438    fn test_round_trip_non_ascii_password() {
439        let name = generate_random_string();
440        let entry = entry_new(&name, &name);
441        test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
442    }
443
444    #[test]
445    fn test_entries_with_same_and_different_specifiers() {
446        let name1 = generate_random_string();
447        let name2 = generate_random_string();
448        let entry1 = entry_new(&name1, &name2);
449        let entry2 = entry_new(&name1, &name2);
450        let entry3 = entry_new(&name2, &name1);
451        entry1.set_password("test password").unwrap();
452        let pw2 = entry2.get_password().unwrap();
453        assert_eq!(pw2, "test password");
454        _ = entry3.get_password().unwrap_err();
455        entry1.delete_credential().unwrap();
456        _ = entry2.get_password().unwrap_err();
457        entry3.delete_credential().unwrap_err();
458    }
459
460    #[test]
461    fn test_get_credential_and_specifiers() {
462        let name = generate_random_string();
463        let entry1 = entry_new(&name, &name);
464        assert!(matches!(entry1.get_credential(), Err(Error::NoEntry)));
465        entry1.set_password("password for entry1").unwrap();
466        let wrapper = entry1.get_credential().unwrap();
467        let (service, user) = wrapper.get_specifiers().unwrap();
468        assert_eq!(service, name);
469        assert_eq!(user, name);
470        wrapper.delete_credential().unwrap();
471        entry1.delete_credential().unwrap_err();
472        wrapper.delete_credential().unwrap_err();
473    }
474
475    #[test]
476    fn test_round_trip_random_secret() {
477        let name = generate_random_string();
478        let entry = entry_new(&name, &name);
479        let secret = generate_random_bytes();
480        test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
481    }
482
483    #[test]
484    fn test_update() {
485        let name = generate_random_string();
486        let entry = entry_new(&name, &name);
487        test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
488        test_round_trip(
489            "updated non-ascii password",
490            &entry,
491            "このきれいな花は桜です",
492        );
493    }
494
495    #[test]
496    fn test_set_error() {
497        let name = generate_random_string();
498        let entry = entry_new(&name, &name);
499        let password = "test ascii password";
500        let mock: &Cred = entry.inner.as_any().downcast_ref().unwrap();
501        mock.set_error(Error::Invalid(
502            "mock error".to_string(),
503            "is an error".to_string(),
504        ));
505        assert!(matches!(
506            entry.set_password(password),
507            Err(Error::Invalid(_, _))
508        ));
509        entry.set_password(password).unwrap();
510        mock.set_error(Error::NoEntry);
511        assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
512        let stored_password = entry.get_password().unwrap();
513        assert_eq!(stored_password, password);
514        mock.set_error(Error::TooLong("mock".to_string(), 3));
515        assert!(matches!(
516            entry.delete_credential(),
517            Err(Error::TooLong(_, 3))
518        ));
519        entry.delete_credential().unwrap();
520        assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
521    }
522
523    #[test]
524    fn test_search() {
525        let store: Arc<CredentialStore> = Store::new().unwrap();
526        let all = store.search(&HashMap::from([])).unwrap();
527        assert!(all.is_empty());
528        let all = store
529            .search(&HashMap::from([("service", ""), ("user", "")]))
530            .unwrap();
531        assert!(all.is_empty());
532        let e1 = store.build("foo", "bar", None).unwrap();
533        e1.set_password("e1").unwrap();
534        let all = store.search(&HashMap::from([])).unwrap();
535        assert_eq!(all.len(), 1);
536        let all = store
537            .search(&HashMap::from([("service", ""), ("user", "")]))
538            .unwrap();
539        assert_eq!(all.len(), 1);
540        let e2 = store.build("foo", "bam", None).unwrap();
541        e2.set_password("e2").unwrap();
542        let one = store.search(&HashMap::from([("user", "m")])).unwrap();
543        assert_eq!(one.len(), 1);
544        let one = store
545            .search(&HashMap::from([("service", "foo"), ("user", "bar")]))
546            .unwrap();
547        assert_eq!(one.len(), 1);
548        let two = store.search(&HashMap::from([("service", "foo")])).unwrap();
549        assert_eq!(two.len(), 2);
550        let all = store.search(&HashMap::from([("foo", "bar")])).unwrap();
551        assert_eq!(all.len(), 2);
552    }
553
554    #[test]
555    fn test_persistence() {
556        let store: Arc<CredentialStore> = Store::new().unwrap();
557        assert!(matches!(
558            store.persistence(),
559            CredentialPersistence::ProcessOnly
560        ))
561    }
562}