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